import React, { useState, useEffect, useRef, useMemo } from 'react'; import { LayoutDashboard, Calendar, BookOpen, FileText, Settings, LogOut, Sun, Moon, ChevronLeft, CheckCircle, BrainCircuit, ListChecks, FileSpreadsheet, Upload, Image as ImageIcon, Users, FileDown, UserPlus, Trash2, User as UserIcon, Save, Loader2, AlertTriangle, Lock, ArrowRight, PlusCircle, XCircle, Printer, GraduationCap, Search, Edit, Download, Activity, MapPin, Phone, Award, Library, Briefcase, ScrollText, ClipboardList, UserCheck, Clock, CalendarDays, Crown, ShieldAlert, Maximize, Minimize } from 'lucide-react'; // --- FIREBASE IMPORTS --- import { initializeApp } from 'firebase/app'; import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged, User as FirebaseUser } from 'firebase/auth'; import { getFirestore, doc, collection, setDoc, onSnapshot, deleteDoc, writeBatch, query, where, addDoc } from 'firebase/firestore'; // --- API CONFIGURATION --- const apiKey = ""; // --- DATA & TYPES --- type Role = 'admin' | 'guru'; type UserData = { id: string; username: string; password?: string; role: Role; name: string; nip?: string; ttl?: string; pangkat?: string; foto?: string | null; isLoggedIn?: boolean; lastLogin?: string; gelarDepan?: string; gelarBelakang?: string; jenisKelamin?: 'L' | 'P'; tempatLahir?: string; tanggalLahir?: string; nik?: string; nuptk?: string; statusKepegawaian?: 'PNS' | 'PPPK' | 'Honorer' | 'GTY' | 'GTT'; agama?: string; alamatLengkap?: string; noHp?: string; email?: string; unitKerja?: string; jabatan?: string; mapelDiampu?: string; kelasDiampu?: string; tmt?: string; masaKerja?: string; statusAktif?: 'Aktif' | 'Cuti' | 'Nonaktif'; jamMengajar?: string; pendidikanTerakhir?: 'SMA' | 'D3' | 'S1' | 'S2' | 'S3'; programStudi?: string; perguruanTinggi?: string; tahunLulus?: string; isSertifikasi?: boolean; nomorSertifikat?: string; tahunSertifikasi?: string; tugasTambahan?: string; riwayatKesehatan?: string; riwayatPendidikan?: string; // Trial Feature isTrial?: boolean; usageCounts?: Record; }; type SchoolData = { dinas: string; namaSekolah: string; alamatSekolah: string; kepalaSekolah: string; nipKepala: string; namaGuru: string; nipGuru: string; tahunAjaran: string; logoUrl: string | null; }; type StudentData = { id: string; nis: string; nama: string; kelas: string; jenisKelamin: 'L' | 'P'; wali: string; }; type GradeData = { id: string; studentId: string; mapel: string; nh: number; tugas: number; pts: number; pas: number; nilaiAkhir: number; predikat: 'A' | 'B' | 'C' | 'D'; deskripsi: string; }; type ScheduleItem = { id: string; day: string; timeStart: string; timeEnd: string; mapel: string; kelas: string; userId: string; }; type CalendarEvent = { id: string; title: string; date: string; type: 'Libur' | 'Ujian' | 'Kegiatan' | 'Rapat'; }; // --- DEFAULT DATA (Seeder) --- const INITIAL_USERS: UserData[] = [ { id: 'admin_default', username: 'EduAdmin', password: '123', role: 'admin', name: 'Administrator Sekolah' }, { id: 'guru_default', username: 'guru', password: '123', role: 'guru', name: 'Budi Santoso', gelarBelakang: 'S.Pd.', nip: '19850101 201001 1 001', tempatLahir: 'Jakarta', tanggalLahir: '1985-01-01', jenisKelamin: 'L', statusKepegawaian: 'PNS', unitKerja: 'SD Negeri Contoh 01', jabatan: 'Guru Kelas', foto: null, isTrial: false, usageCounts: {} } ]; const INITIAL_SCHOOL_DATA: SchoolData = { dinas: 'DINAS PENDIDIKAN PROVINSI', namaSekolah: 'SEKOLAH CONTOH INDONESIA', alamatSekolah: 'Jl. Pendidikan No. 1, Jakarta', kepalaSekolah: 'Dr. H. Ahmad Hidayat, M.Pd.', nipKepala: '19680101 199003 1 005', namaGuru: '..........................', nipGuru: '..........................', tahunAjaran: '2025/2026', logoUrl: null }; // --- MULTI-JENJANG CONFIGURATION --- const JENJANG_OPTIONS = ['PAUD', 'SD', 'SMP', 'SMA', 'SMK']; const MAPEL_BY_JENJANG: Record = { 'PAUD': ["Nilai Agama dan Budi Pekerti", "Jati Diri", "Dasar Literasi dan STEAM", "Projek Penguatan Profil Pelajar Pancasila"], 'SD': ["Pendidikan Agama dan Budi Pekerti", "Pendidikan Pancasila", "Bahasa Indonesia", "Matematika", "IPAS", "PJOK", "Seni Budaya", "Bahasa Inggris", "Muatan Lokal"], 'SMP': ["Pendidikan Agama dan Budi Pekerti", "Pendidikan Pancasila", "Bahasa Indonesia", "Matematika", "IPA", "IPS", "Bahasa Inggris", "PJOK", "Informatika", "Seni Budaya", "Prakarya"], 'SMA': ["Pendidikan Agama dan Budi Pekerti", "Pendidikan Pancasila", "Bahasa Indonesia", "Matematika (Wajib)", "Matematika (Lanjut)", "Bahasa Inggris", "Fisika", "Kimia", "Biologi", "Ekonomi", "Sosiologi", "Geografi", "Sejarah", "PJOK", "Informatika", "Seni Budaya"], 'SMK': ["Pendidikan Agama dan Budi Pekerti", "Pendidikan Pancasila", "Bahasa Indonesia", "Matematika", "Bahasa Inggris", "Sejarah", "PJOK", "Seni Budaya", "Projek IPAS", "Informatika", "Dasar Program Keahlian", "Konsentrasi Keahlian", "Produk Kreatif dan Kewirausahaan (PKK)"] }; const KELAS_BY_JENJANG: Record = { 'PAUD': ['Kelompok Bermain (KB)', 'TK A', 'TK B'], 'SD': ['1', '2', '3', '4', '5', '6'], 'SMP': ['7', '8', '9'], 'SMA': ['10', '11', '12'], 'SMK': ['10', '11', '12', '13'] }; const KONTEKS_OPTIONS = ["Umum / General", "Kehidupan Sehari-hari", "Lingkungan Sekolah", "Sains & Teknologi", "Sosial Budaya", "Cerita Naratif / Dongeng", "Studi Kasus Nyata", "Isu Global / Lingkungan", "Kearifan Lokal", "Dunia Kerja / Industri"]; const BENTUK_SOAL_OPTIONS = ["Pilihan Ganda", "Benar/Salah", "Menjodohkan", "Isian Singkat", "Pilihan Ganda Kompleks", "Uraian / Esai"]; const MODEL_PEMBELAJARAN_OPTIONS = ["Problem-Based Learning (PBL)", "Project-Based Learning (PJBL)", "Discovery Learning", "Inquiry Learning", "Cooperative Learning", "Contextual Teaching and Learning (CTL)", "Teaching Factory (TEFA)", "Game-Based Learning", "Sentra / Area (PAUD)"]; const P5_THEMES = ["Gaya Hidup Berkelanjutan", "Kearifan Lokal", "Bhinneka Tunggal Ika", "Bangunlah Jiwa dan Raganya", "Suara Demokrasi", "Rekayasa dan Teknologi", "Kewirausahaan", "Kebekerjaan"]; // --- AI SERVICE --- const callGeminiAI = async (prompt: string) => { const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`; const payload = { contents: [{ parts: [{ text: prompt }] }], generationConfig: { responseMimeType: "application/json" } }; try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); const data = await response.json(); const textResponse = data.candidates?.[0]?.content?.parts?.[0]?.text; if (!textResponse) throw new Error("Tidak ada respon dari AI."); return JSON.parse(textResponse.replace(/^```json\s*/, '').replace(/\s*```$/, '')); } catch (error) { console.error("AI Error:", error); throw error; } }; // --- HELPER COMPONENTS --- const InputField = ({ label, value, onChange, placeholder, type = "text" }: any) => (
{label && }
); const TextAreaField = ({ label, value, onChange, placeholder, rows=3 }: any) => (
{label && }
)} {activeTab === 'kepegawaian' && (

Data Kepegawaian

setFormData({...formData,nip: e.target.value})} /> setFormData({...formData, nuptk: e.target.value})} /> setFormData({...formData, statusKepegawaian: e.target.value})} options={['PNS', 'PPPK', 'Honorer', 'GTY', 'GTT']} /> setFormData({...formData, pangkat: e.target.value})} /> setFormData({...formData, unitKerja: e.target.value})} /> setFormData({...formData, jabatan: e.target.value})} /> setFormData({...formData, tmt: e.target.value})} /> setFormData({...formData, statusAktif: e.target.value})} options={['Aktif', 'Cuti', 'Nonaktif']} />
)} {activeTab === 'pendidikan' && (

Pendidikan & Sertifikasi

Pendidikan Terakhir

setFormData({...formData, pendidikanTerakhir: e.target.value})} options={['SMA', 'D3', 'S1', 'S2', 'S3']} /> setFormData({...formData, tahunLulus: e.target.value})} /> setFormData({...formData, perguruanTinggi: e.target.value})} /> setFormData({...formData, programStudi: e.target.value})} />
)} {activeTab === 'dokumen' && (

Dokumen & Data Lain

{['SK Pengangkatan', 'Ijazah Terakhir', 'KTP', 'NPWP', 'Sertifikat Pendidik'].map((doc, idx) => (
{doc} Sudah Upload
))}

*Fitur upload file dinonaktifkan pada versi demo ini.

setFormData({...formData, tugasTambahan: e.target.value})} placeholder="Contoh: Wali Kelas VIA, Pembina Pramuka..." /> setFormData({...formData, riwayatKesehatan: e.target.value})} placeholder="Riwayat penyakit atau alergi..." />
)}
); } // ... StudentManagement ... function StudentManagement({ db, appId }: { db: any, appId: string }) { const [students, setStudents] = useState([]); const [formData, setFormData] = useState>({ nis: '', nama: '', kelas: '', jenisKelamin: 'L', wali: '' }); const [isEditing, setIsEditing] = useState(false); const [search, setSearch] = useState(''); const [loading, setLoading] = useState(false); useEffect(() => { const unsub = onSnapshot(collection(db, 'artifacts', appId, 'public', 'data', 'students'), (snap) => { setStudents(snap.docs.map(d => d.data() as StudentData)); }); return () => unsub(); }, [db, appId]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); const id = isEditing && formData.id ? formData.id : Date.now().toString(); const data: StudentData = { id, nis: formData.nis!, nama: formData.nama!, kelas: formData.kelas!, jenisKelamin: formData.jenisKelamin as 'L'|'P', wali: formData.wali || '' }; await setDoc(doc(db, 'artifacts', appId, 'public', 'data', 'students', id), data); setFormData({ nis: '', nama: '', kelas: '', jenisKelamin: 'L', wali: '' }); setIsEditing(false); setLoading(false); }; const handleEdit = (s: StudentData) => { setFormData(s); setIsEditing(true); }; const handleDelete = async (id: string) => { if(confirm('Yakin ingin menghapus siswa ini?')) { await deleteDoc(doc(db, 'artifacts', appId, 'public', 'data', 'students', id)); } }; const filteredStudents = students.filter(s => (s.nama || '').toLowerCase().includes(search.toLowerCase()) || (s.nis || '').includes(search) || (s.kelas || '').toLowerCase().includes(search.toLowerCase()) ); return (

{isEditing ? : } {isEditing ? 'Edit Data Siswa' : 'Tambah Siswa Manual'}

setFormData({...formData, nis: e.target.value})} className="lg:col-span-1" /> setFormData({...formData, nama: e.target.value})} className="lg:col-span-1" /> setFormData({...formData, kelas: e.target.value})} className="lg:col-span-1" />
{isEditing && }

Data Siswa ({filteredStudents.length})

setSearch(e.target.value)} className="pl-9 pr-4 py-2 border rounded-full text-sm bg-slate-50 dark:bg-slate-900 dark:text-white focus:ring-2 focus:ring-blue-500 outline-none w-64"/>
{filteredStudents.length > 0 ? filteredStudents.map((s, idx) => ( )) : ( )}
No NIS Nama Siswa L/P Kelas Aksi
{idx+1} {s.nis} {s.nama} {s.jenisKelamin} {s.kelas}
Belum ada data siswa. Silakan tambahkan.
); } // ... GradingSystem ... function GradingSystem({ db, appId, school }: { db: any, appId: string, school: SchoolData }) { const [students, setStudents] = useState([]); const [grades, setGrades] = useState>({}); const [params, setParams] = useState({ jenjang: 'SD', kelas: '6', mapel: 'Matematika', semester: '1' }); const [loading, setLoading] = useState(false); const [printing, setPrinting] = useState(false); useEffect(() => { const unsubStudents = onSnapshot(collection(db, 'artifacts', appId, 'public', 'data', 'students'), (snap) => { setStudents(snap.docs.map(d => d.data() as StudentData).filter(s => s.kelas === params.kelas)); }); const unsubGrades = onSnapshot(collection(db, 'artifacts', appId, 'public', 'data', 'grades'), (snap) => { const gradesData: Record = {}; snap.docs.forEach(d => { const data = d.data() as GradeData; gradesData[d.id] = data; }); setGrades(gradesData); }); return () => { unsubStudents(); unsubGrades(); }; }, [db, appId, params.kelas]); const handleJenjangChange = (newJenjang: string) => { setParams({ ...params, jenjang: newJenjang, mapel: MAPEL_BY_JENJANG[newJenjang][0], kelas: KELAS_BY_JENJANG[newJenjang][0] }); }; const calculateGrade = (nh: number, tugas: number, pts: number, pas: number) => { const final = (nh * 0.2) + (tugas * 0.2) + (pts * 0.3) + (pas * 0.3); let predikat: 'A'|'B'|'C'|'D' = 'D'; let deskripsi = 'Perlu bimbingan lebih lanjut.'; if (final >= 90) { predikat = 'A'; deskripsi = 'Sangat baik dalam memahami materi.'; } else if (final >= 80) { predikat = 'B'; deskripsi = 'Baik dalam memahami materi.'; } else if (final >= 70) { predikat = 'C'; deskripsi = 'Cukup memahami materi.'; } return { final: parseFloat(final.toFixed(1)), predikat, deskripsi }; }; const updateGrade = (studentId: string, field: keyof GradeData, value: number) => { const id = `${studentId}_${params.mapel}`; const current = grades[id] || { id, studentId, mapel: params.mapel, nh: 0, tugas: 0, pts: 0, pas: 0, nilaiAkhir: 0, predikat: 'D', deskripsi: '' }; const newData = { ...current, [field]: value }; const calc = calculateGrade(newData.nh, newData.tugas, newData.pts, newData.pas); setGrades(prev => ({ ...prev, [id]: { ...newData, nilaiAkhir: calc.final, predikat: calc.predikat, deskripsi: calc.deskripsi } })); }; const saveGrades = async () => { setLoading(true); try { const batch = writeBatch(db); Object.values(grades).forEach(g => { const docRef = doc(db, 'artifacts', appId, 'public', 'data', 'grades', g.id); batch.set(docRef, g); }); await batch.commit(); alert('Data Nilai Berhasil Disimpan Secara Permanen!'); } catch (error) { console.error("Gagal menyimpan nilai:", error); alert('Terjadi kesalahan saat menyimpan nilai. Periksa koneksi internet Anda.'); } finally { setLoading(false); } }; return (

Konfigurasi Penilaian

handleJenjangChange(e.target.value)} options={JENJANG_OPTIONS} /> setParams({...params, kelas: e.target.value})} options={KELAS_BY_JENJANG[params.jenjang]} /> setParams({...params, mapel: e.target.value})} options={MAPEL_BY_JENJANG[params.jenjang]} /> setParams({...params, semester: e.target.value})} options={['1', '2']} />
{printing && (

{school.dinas}

{school.namaSekolah}

{school.alamatSekolah}

DAFTAR NILAI SISWA

Mata Pelajaran: {params.mapel}Kelas: {params.kelas}
Tahun Ajaran: {school.tahunAjaran}Semester: {params.semester}
{students.map((s, idx) => { const g = grades[`${s.id}_${params.mapel}`] || {}; return ( ); })}
No NIS Nama Siswa NH Tugas PTS PAS Akhir Predikat
{idx + 1} {s.nis} {s.nama} {g.nh || 0} {g.tugas || 0} {g.pts || 0} {g.pas || 0} {g.nilaiAkhir || 0} {g.predikat || 'D'}

Mengetahui,

Kepala Sekolah

{school.kepalaSekolah}

NIP. {school.nipKepala}

Jakarta, {new Date().toLocaleDateString('id-ID', {day: 'numeric', month: 'long', year: 'numeric'})}

Guru Mata Pelajaran

{school.namaGuru}

NIP. {school.nipGuru}

)}

Input Nilai: {params.mapel} - Kelas {params.kelas}

{students.length > 0 ? students.map((s, idx) => { const g = grades[`${s.id}_${params.mapel}`] || { nh: 0, tugas: 0, pts: 0, pas: 0, nilaiAkhir: 0, predikat: '-' }; return ( ); }) : }
No Nama Siswa NH (20%) Tugas (20%) PTS (30%) PAS (30%) Akhir Pred
{idx + 1} {s.nama}
{s.nis}
updateGrade(s.id, 'nh', parseInt(e.target.value) || 0)} /> updateGrade(s.id, 'tugas', parseInt(e.target.value) || 0)} /> updateGrade(s.id, 'pts', parseInt(e.target.value) || 0)} /> updateGrade(s.id, 'pas', parseInt(e.target.value) || 0)} /> {g.nilaiAkhir || 0} {g.predikat || '-'}
Tidak ada siswa di kelas ini.
); } // ... JadwalPelajaran ... function JadwalPelajaran({ db, appId, user, school, onPrint, onExportWord }: { db: any, appId: string, user: UserData, school: SchoolData, onPrint: () => void, onExportWord: () => void }) { const [schedule, setSchedule] = useState([]); const [newItem, setNewItem] = useState({ day: 'Senin', timeStart: '07:00', timeEnd: '08:20', mapel: '', kelas: '' }); const [loading, setLoading] = useState(false); const [printing, setPrinting] = useState(false); const days = ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu']; useEffect(() => { if(!user) return; const q = collection(db, 'artifacts', appId, 'public', 'data', 'schedule'); const unsub = onSnapshot(q, (snap) => { const data = snap.docs.map(d => ({ id: d.id, ...d.data() } as ScheduleItem)); const filtered = user.role === 'guru' ? data.filter(i => i.userId === user.id) : data; setSchedule(filtered); }); return () => unsub(); }, [db, appId, user]); const handleAdd = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); if (newItem.mapel && newItem.kelas) { await addDoc(collection(db, 'artifacts', appId, 'public', 'data', 'schedule'), { ...newItem, userId: user.id }); setNewItem({ ...newItem, mapel: '', kelas: '' }); } setLoading(false); }; const handleDelete = async (id: string) => { if(confirm('Hapus jadwal ini?')) { await deleteDoc(doc(db, 'artifacts', appId, 'public', 'data', 'schedule', id)); } }; if (printing) { return (
{days.map(day => { const dayItems = schedule.filter(s => s.day === day).sort((a,b) => a.timeStart.localeCompare(b.timeStart)); if (dayItems.length === 0) return null; return (

{day}

{dayItems.map(item => ( ))}
Waktu Mata Pelajaran Kelas
{item.timeStart} - {item.timeEnd} {item.mapel} {item.kelas}
); })}
); } return (

Jadwal Pelajaran Aktif

Input Jadwal Baru

setNewItem({...newItem, day: e.target.value})} options={days} /> setNewItem({...newItem, timeStart: e.target.value})} /> setNewItem({...newItem, timeEnd: e.target.value})} /> setNewItem({...newItem, mapel: e.target.value})} placeholder="Matematika" /> setNewItem({...newItem, kelas: e.target.value})} placeholder="X-A" />
{days.map(day => { const dayItems = schedule.filter(s => s.day === day).sort((a,b) => a.timeStart.localeCompare(b.timeStart)); return (
{day}
{dayItems.length > 0 ? dayItems.map(item => (
{item.timeStart} - {item.timeEnd}
{item.mapel}
Kelas {item.kelas}
)) :
Kosong
}
); })}
); } // ... KalenderAkademik ... function KalenderAkademik({ db, appId, school, user, onPrint, onExportWord }: { db: any, appId: string, school: SchoolData, user: UserData, onPrint: () => void, onExportWord: () => void }) { const [events, setEvents] = useState([]); const [newEvent, setNewEvent] = useState>({ title: '', date: new Date().toISOString().split('T')[0], type: 'Kegiatan' }); const [loading, setLoading] = useState(false); const [printing, setPrinting] = useState(false); const [currentDate, setCurrentDate] = useState(new Date()); useEffect(() => { const q = collection(db, 'artifacts', appId, 'public', 'data', 'calendar_events'); const unsub = onSnapshot(q, (snap) => { setEvents(snap.docs.map(d => ({ id: d.id, ...d.data() } as CalendarEvent))); }); return () => unsub(); }, [db, appId]); const handleAdd = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); if (newEvent.title && newEvent.date) { await addDoc(collection(db, 'artifacts', appId, 'public', 'data', 'calendar_events'), newEvent); setNewEvent({ ...newEvent, title: '' }); } setLoading(false); }; const handleDelete = async (id: string) => { if(confirm('Hapus agenda ini?')) await deleteDoc(doc(db, 'artifacts', appId, 'public', 'data', 'calendar_events', id)); }; const getDaysInMonth = (year: number, month: number) => new Date(year, month + 1, 0).getDate(); const getFirstDayOfMonth = (year: number, month: number) => new Date(year, month, 1).getDay(); const year = currentDate.getFullYear(); const month = currentDate.getMonth(); const daysInMonth = getDaysInMonth(year, month); const firstDay = getFirstDayOfMonth(year, month); const blanks = Array(firstDay).fill(null); const days = Array.from({ length: daysInMonth }, (_, i) => i + 1); const monthNames = ["Januari", "Februari", "Maret", "April", "Mei", "Juni", "Juli", "Agustus", "September", "Oktober", "November", "Desember"]; if (printing) { return (
{events.sort((a,b) => a.date.localeCompare(b.date)).map((ev, idx) => { const dateObj = new Date(ev.date); const dateFormatted = dateObj.toLocaleDateString('id-ID', { day: 'numeric', month: 'long', year: 'numeric' }); return ( ); })} {events.length === 0 && }
No Tanggal Agenda Kegiatan Kategori
{idx + 1} {dateFormatted} {ev.title} {ev.type}
Belum ada agenda.
); } return (

Tambah Agenda

setNewEvent({...newEvent, title: e.target.value})} /> setNewEvent({...newEvent, date: e.target.value})} /> setNewEvent({...newEvent, type: e.target.value as any})} options={['Kegiatan', 'Libur', 'Ujian', 'Rapat']} />

Agenda Bulan Ini

{events .filter(e => e.date.startsWith(`${year}-${String(month+1).padStart(2,'0')}`)) .sort((a,b) => a.date.localeCompare(b.date)) .map(e => (
{e.date.split('-')[2]}
{e.title}
{e.type}
))} {events.filter(e => e.date.startsWith(`${year}-${String(month+1).padStart(2,'0')}`)).length === 0 && (

Tidak ada agenda.

)}

{monthNames[month]} {year}

{['Min', 'Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab'].map(d =>
{d}
)}
{blanks.map((_, i) =>
)} {days.map(d => { const dateStr = `${year}-${String(month+1).padStart(2,'0')}-${String(d).padStart(2,'0')}`; const dayEvents = events.filter(e => e.date === dateStr); const isToday = new Date().toDateString() === new Date(year, month, d).toDateString(); return (
{d}
{dayEvents.map(ev => (
{ev.title}
))}
); })}
); } // ... AdminMonitoring ... function AdminMonitoring({ db, appId }: { db: any, appId: string }) { return (

Monitoring Kinerja

Fitur monitoring kinerja guru sedang dalam pengembangan. Anda akan dapat melihat statistik aktivitas guru di sini.

); } // ... UserManagement ... function UserManagement({ users, onAdd, onDelete }: { users: UserData[], onAdd: (u: UserData) => void, onDelete: (id: string) => void }) { const [newUser, setNewUser] = useState>({ username: '', password: '', name: '', role: 'guru', nip: '', statusKepegawaian: 'Honorer', isTrial: false }); const [notification, setNotification] = useState(''); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (newUser.username && newUser.password && newUser.name) { onAdd({ ...newUser, id: Date.now().toString(), usageCounts: {} } as UserData); setNewUser({ username: '', password: '', name: '', role: 'guru', nip: '', statusKepegawaian: 'Honorer', isTrial: false }); setNotification('User berhasil ditambahkan dan tersimpan otomatis!'); setTimeout(() => setNotification(''), 3000); } }; return (

Tambah User Baru

{notification && (
{notification}
)}
setNewUser({...newUser, name: e.target.value})} /> setNewUser({...newUser, username: e.target.value})} /> setNewUser({...newUser, password: e.target.value})} /> setNewUser({...newUser, nip: e.target.value})} />
setNewUser({...newUser, isTrial: e.target.checked})} className="w-4 h-4 text-blue-600 rounded" />
{users.map(user => ())}
Nama / NIPStatusJabatanAksi
{user.name} {user.isTrial && TRIAL}
{user.nip || '-'}
{user.statusKepegawaian || '-'}{user.jabatan || '-'}{user.username !== 'EduAdmin' && }
); } // ... SettingsPage ... function SettingsPage({ data, setData }: { data: SchoolData, setData: (d: SchoolData) => void }) { const fileInputRef = useRef(null); const handleLogo = (e: React.ChangeEvent) => { if (e.target.files && e.target.files[0]) { const reader = new FileReader(); reader.onloadend = () => setData({ ...data, logoUrl: reader.result as string }); reader.readAsDataURL(e.target.files[0]); } }; const handleChange = (k: keyof SchoolData, v: string) => setData({ ...data, [k]: v }); return (

Pengaturan Sekolah

{data.logoUrl ? : }
handleChange('dinas', e.target.value)} placeholder="Contoh: DINAS PENDIDIKAN PROVINSI DKI JAKARTA" /> handleChange('namaSekolah', e.target.value)} /> handleChange('alamatSekolah', e.target.value)} />
handleChange('kepalaSekolah', e.target.value)} /> handleChange('nipKepala', e.target.value)} />
handleChange('tahunAjaran', e.target.value)} />
); } // ... GeneratorPerencanaan ... function GeneratorPerencanaan({ school, user, onPrint, onExportWord, title = "Program Tahunan", onUpdateUser }: any) { const isProta = title === "Program Tahunan"; const isPromes = title === "Program Semester"; const isATP = title === "Alur Tujuan Pembelajaran"; const isModulP5 = title === "Modul P5"; const isLKPD = title === "Lembar Kerja Peserta Didik"; if (!isProta && !isPromes && !isATP && !isModulP5 && !isLKPD) { return (

{title}

Modul ini sedang dalam pengembangan. Silakan gunakan fitur Program Tahunan, Program Semester, atau Alur Tujuan Pembelajaran.

); } const [params, setParams] = useState({ jenjang: 'SD', kelas: '6', mapel: isModulP5 ? P5_THEMES[0] : 'Matematika', topik: '', fase: 'C', semester: '1', tahun: school.tahunAjaran || '2025/2026' }); const [loading, setLoading] = useState(false); const [generated, setGenerated] = useState(false); const [content, setContent] = useState(isProta ? { semester1: [], semester2: [] } : []); const [errorMsg, setErrorMsg] = useState(''); const [showUpgrade, setShowUpgrade] = useState(false); const handleJenjangChange = (newJenjang: string) => { setParams({ ...params, jenjang: newJenjang, mapel: isModulP5 ? P5_THEMES[0] : MAPEL_BY_JENJANG[newJenjang][0], kelas: KELAS_BY_JENJANG[newJenjang][0] }); }; const handleGen = async () => { // TRIAL CHECK LOGIC const featureKey = isProta ? 'prota' : isPromes ? 'promes' : isATP ? 'atp' : isModulP5 ? 'modul_p5' : 'lkpd'; if (user.isTrial) { const currentUsage = user.usageCounts?.[featureKey] || 0; if (currentUsage >= 3) { setShowUpgrade(true); return; } } setLoading(true); setErrorMsg(''); setGenerated(false); let prompt = ""; // ... Prompt Logic ... if (isProta) { prompt = ` Bertindaklah sebagai Guru Profesional dan Pakar Kurikulum Merdeka. Buatkan PROGRAM TAHUNAN (PROTA) lengkap untuk: - Mata Pelajaran: ${params.mapel} - Kelas: ${params.kelas} - Jenjang: ${params.jenjang} - Fase: ${params.fase} - Tahun Ajaran: ${params.tahun} Tugas: Bagilah materi pelajaran selama satu tahun menjadi Semester 1 dan Semester 2. Untuk setiap semester, buatkan daftar materi pokok beserta alokasi waktu yang logis. Output WAJIB berupa JSON valid (tanpa markdown) dengan struktur berikut: { "semester1": [ { "no": 1, "tujuan_pembelajaran": "...", "materi_pokok": "...", "alokasi_waktu": "12 JP" }, { "no": 2, "tujuan_pembelajaran": "...", "materi_pokok": "...", "alokasi_waktu": "..." } ], "semester2": [ { "no": 1, "tujuan_pembelajaran": "...", "materi_pokok": "...", "alokasi_waktu": "..." } ] } `; } else if (isPromes) { prompt = ` Buatkan PROGRAM SEMESTER (PROMES) untuk Semester ${params.semester} (Ganjil/Genap). Mapel: ${params.mapel}, Kelas: ${params.kelas}. Bulan efektif: Juli s.d. Desember (Ganjil) atau Januari s.d. Juni (Genap). Output JSON Valid (Pastikan format JSON bersih tanpa error): { "list": [ { "no": 1, "materi": "Bab 1: Bilangan Cacah Besar", "alokasi": "12 JP", "ket": "Juli Minggu 3-4" }, { "no": 2, "materi": "Bab 2: Pembagian", "alokasi": "8 JP", "ket": "Agustus Minggu 1-2" }, { "no": 3, "materi": "Sumatif Tengah Semester", "alokasi": "2 JP", "ket": "September Minggu 3" } ] } Pastikan materi urut, logis, dan mencakup asesmen sumatif. `; } else if (isATP) { prompt = ` Buatkan ALUR TUJUAN PEMBELAJARAN (ATP) untuk Fase ${params.fase} Kelas ${params.kelas} Mapel ${params.mapel}. Output JSON Valid: { "list": [ { "elemen": "Bilangan", "cp": "Peserta didik dapat...", "tp": "Memahami...", "p3": "Bernalar kritis", "alokasi": "10 JP" }, { "elemen": "Aljabar", "cp": "...", "tp": "...", "p3": "Mandiri", "alokasi": "..." } ] } `; } else if (isModulP5) { prompt = ` Buatkan MODUL PROJEK PENGUATAN PROFIL PELAJAR PANCASILA (P5) yang Lengkap. - Tema: ${params.mapel} - Fase: ${params.fase} (Kelas ${params.kelas}) - Jenjang: ${params.jenjang} Output JSON Valid: { "identitas": { "topik_spesifik": "Contoh: Sampahku Tanggung Jawabku", "deskripsi_singkat": "Penjelasan singkat projek..." }, "dimensi": [ { "nama": "Beriman, bertakwa...", "elemen": "Akhlak kepada alam", "target": "Memahami keterhubungan ekosistem..." }, { "nama": "Gotong Royong", "elemen": "Kolaborasi", "target": "..." } ], "alur_aktivitas": [ { "tahap": "Pengenalan", "kegiatan": "Sosialisasi isu lingkungan...", "jp": "4 JP" }, { "tahap": "Kontekstualisasi", "kegiatan": "Kunjungan ke TPA...", "jp": "6 JP" }, { "tahap": "Aksi", "kegiatan": "Membuat produk daur ulang...", "jp": "12 JP" }, { "tahap": "Refleksi", "kegiatan": "Pameran karya...", "jp": "8 JP" } ], "asesmen": { "jenis": "Sumatif melalui pameran karya", "instrumen": "Rubrik Penilaian" } } `; } else if (isLKPD) { prompt = ` Buatkan LEMBAR KERJA PESERTA DIDIK (LKPD) lengkap untuk: - Jenjang: ${params.jenjang} - Kelas: ${params.kelas} - Mapel: ${params.mapel} - Topik: "${params.topik || 'Materi Umum'}" Berikan output JSON valid: { "judul": "...", "tujuan": "...", "petunjuk": ["..."], "alat_bahan": ["..."], "langkah_kegiatan": ["..."], "soal": ["Soal 1...", "Soal 2..."] } `; } try { const result = await callGeminiAI(prompt); if (result) { setContent(result); setGenerated(true); // INCREMENT USAGE if (user.isTrial) { const newCount = (user.usageCounts?.[featureKey] || 0) + 1; onUpdateUser({ ...user, usageCounts: { ...user.usageCounts, [featureKey]: newCount } }); } } else { throw new Error("Format JSON dari AI tidak sesuai."); } } catch (err) { setErrorMsg(`Gagal membuat ${title}. Coba lagi.`); } finally { setLoading(false); } }; return (
{showUpgrade && setShowUpgrade(false)} />}

Parameter {title}

handleJenjangChange(e.target.value)} options={JENJANG_OPTIONS} /> {isModulP5 ? ( setParams({...params, mapel: e.target.value})} options={P5_THEMES} /> ) : ( setParams({...params, mapel: e.target.value})} options={MAPEL_BY_JENJANG[params.jenjang]} /> )} {isLKPD && setParams({...params, topik: e.target.value})} />}
setParams({...params, kelas: e.target.value})} options={KELAS_BY_JENJANG[params.jenjang]} /> setParams({...params, fase: e.target.value})} options={['A','B','C','D','E','F']} />
{isPromes && setParams({...params, semester: e.target.value})} options={['1 (Ganjil)', '2 (Genap)']} />} setParams({...params, tahun: e.target.value})} /> {user.isTrial && (

Sisa Percobaan: {3 - (user.usageCounts?.[isProta ? 'prota' : isPromes ? 'promes' : isATP ? 'atp' : isModulP5 ? 'modul_p5' : 'lkpd'] || 0)} / 3

)} {errorMsg &&
{errorMsg}
}
{generated && ( <> {isProta && ( <>

SEMESTER 1 (GANJIL)

{content.semester1?.map((item: any, idx: number) => ( ))}
No Tujuan Pembelajaran / ATP Materi Pokok Alokasi Waktu
{item.no} {item.tujuan_pembelajaran} {item.materi_pokok} {item.alokasi_waktu}

SEMESTER 2 (GENAP)

{content.semester2?.map((item: any, idx: number) => ( ))}
No Tujuan Pembelajaran / ATP Materi Pokok Alokasi Waktu
{item.no} {item.tujuan_pembelajaran} {item.materi_pokok} {item.alokasi_waktu}
)} {isPromes && (

Semester: {params.semester}

{content.list?.map((item: any, idx: number) => ( ))}
No Materi Pokok / Bab Alokasi Bulan/Minggu Ke- 12345
{item.no} {item.materi} {item.alokasi} {item.ket}

* Kolom 1-5 menunjukkan minggu ke- dalam bulan tersebut.

* Silakan beri tanda centang (✓) atau warna pada kolom minggu yang sesuai setelah dicetak.

)} {isATP && (
{content.list?.map((item: any, idx: number) => ( ))}
Elemen Capaian Pembelajaran (CP) Tujuan Pembelajaran (TP) Profil Pelajar Pancasila Alokasi
{item.elemen} {item.cp} {item.tp} {item.p3} {item.alokasi}
)} {isModulP5 && (

{content.identitas?.topik_spesifik}

{content.identitas?.deskripsi_singkat}

Target & Dimensi

{content.dimensi?.map((item: any, idx: number) => ( ))}
Dimensi Profil Pelajar Pancasila Elemen Target Pencapaian
{item.nama} {item.elemen} {item.target}

Alur Aktivitas Projek

{content.alur_aktivitas?.map((item: any, idx: number) => ( ))}
Tahapan Kegiatan Utama JP
{item.tahap} {item.kegiatan} {item.jp}

Asesmen: {content.asesmen?.jenis}

Instrumen: {content.asesmen?.instrumen}

)} {isLKPD && (

{content.judul}

Nama Kelompok / Siswa: ......................................................

Kelas: {params.kelas}

Hari/Tanggal: ......................................................

A. Tujuan Pembelajaran

{content.tujuan}

B. Petunjuk Belajar

    {content.petunjuk?.map((p:string, i:number) =>
  • {p}
  • )}

C. Alat dan Bahan

    {content.alat_bahan?.map((p:string, i:number) =>
  • {p}
  • )}

D. Langkah Kegiatan

    {content.langkah_kegiatan?.map((p:string, i:number) =>
  • {p}
  • )}

E. Soal Latihan / Diskusi

{content.soal?.map((s:string, i:number) => (

{i+1}. {s}

))}
)}
)}
); } // ... GeneratorModulAjar ... function GeneratorModulAjar({ school, user, onPrint, onExportWord, onUpdateUser }: { school: SchoolData, user: UserData, onPrint: () => void, onExportWord: () => void, onUpdateUser: (u: UserData) => void }) { const [params, setParams] = useState({ jenjang: 'SD', mapel: 'Pendidikan Agama Islam dan Budi Pekerti', topik: 'Indahnya Saling Menghargai', kelas: '6', fase: 'C', semester: '1', alokasi: '3 JP (3 x 40 Menit)', model: 'Problem-Based Learning (PBL)' }); const [generated, setGenerated] = useState(false); const [loading, setLoading] = useState(false); const [content, setContent] = useState({}); const [errorMsg, setErrorMsg] = useState(''); const [showUpgrade, setShowUpgrade] = useState(false); const handleJenjangChange = (newJenjang: string) => { setParams({ ...params, jenjang: newJenjang, mapel: MAPEL_BY_JENJANG[newJenjang][0], kelas: KELAS_BY_JENJANG[newJenjang][0] }); }; const handleGen = async () => { // TRIAL CHECK if (user.isTrial) { const currentUsage = user.usageCounts?.['modul'] || 0; if (currentUsage >= 3) { setShowUpgrade(true); return; } } setLoading(true); setErrorMsg(''); setGenerated(false); const prompt = ` Anda adalah AI Super Admin EduAdmin yang berperan sebagai: ✔ Perancang Modul Ajar Kurikulum Merdeka ✔ Ahli Deep Learning (Pembelajaran Bermakna Mendalam) ✔ Guru Senior Mata Pelajaran ${params.mapel} untuk jenjang ${params.jenjang} Kelas ${params.kelas} TUGAS UTAMA: Buatkan KONTEN MODUL AJAR / RPP LENGKAP untuk: - Jenjang: ${params.jenjang} - Mata Pelajaran: ${params.mapel} - Kelas/Fase: ${params.kelas} / ${params.fase} - Topik: "${params.topik}" - Alokasi Waktu: ${params.alokasi} - Model Pembelajaran: ${params.model} - Pendekatan: Deep Learning KRITERIA WAJIB & STRUKTUR (A-L): A. Identifikasi Murid: Karakteristik, pengetahuan prasyarat, minat. B. Dimensi Profil Lulusan: Dimensi relevan. C. Tujuan Pembelajaran: Operasional, terukur, deep learning oriented. D. Praktik Pedagogik (Deep Learning): Strategi bermakna, kontekstual, reflektif. E. Mitra Pembelajaran: Keterlibatan orang tua/komunitas. F. Lingkungan Pembelajaran: Fisik, sosial, digital. G. Pemanfaatan Digital: Perencanaan, pelaksanaan, asesmen. H. Langkah-Langkah Pembelajaran: Rinci per pertemuan (Pembukaan, Inti, Penutup). I. Asesmen dan Tindak Lanjut: Awal, Formatif, Sumatif. J. Kriteria Keberhasilan Asesmen (TABEL): Jenis, Kriteria, Memenuhi/Belum. K. Rubrik Penilaian (TABEL): Aspek, Skor 4-1. L. Rencana Tindak Lanjut (TABEL): Tuntas/Belum Tuntas. OUTPUT WAJIB JSON VALID (tanpa markdown \`\`\`json) dengan keys: { "informasi_umum": { "identifikasi_murid": "...", "profil_lulusan": "..." }, "komponen_inti": { "tujuan": "...", "praktik_pedagogik": "...", "mitra": "...", "lingkungan": "...", "digital": "..." }, "langkah_pembelajaran": "Teks lengkap langkah pembelajaran...", "asesmen": "Teks penjelasan asesmen...", "tabel_kriteria": [ {"jenis": "...", "kriteria": "...", "memenuhi": "...", "belum": "..."} ], "tabel_rubrik": [ {"aspek": "...", "skor4": "...", "skor3": "...", "skor2": "...", "skor1": "..."} ], "tabel_tindak_lanjut": [ {"kategori": "...", "bentuk": "..."} ] } `; try { const result = await callGeminiAI(prompt); if (result) { setContent(result); setGenerated(true); // INCREMENT USAGE if (user.isTrial) { const newCount = (user.usageCounts?.['modul'] || 0) + 1; onUpdateUser({ ...user, usageCounts: { ...user.usageCounts, ['modul']: newCount } }); } } else { throw new Error("Format JSON dari AI tidak sesuai."); } } catch (err) { setErrorMsg("Gagal membuat modul ajar. Coba lagi."); } finally { setLoading(false); } }; return (
{showUpgrade && setShowUpgrade(false)} />}

Parameter Modul Ajar

handleJenjangChange(e.target.value)} options={JENJANG_OPTIONS} /> setParams({...params, mapel: e.target.value})} options={MAPEL_BY_JENJANG[params.jenjang]} /> setParams({...params, topik: e.target.value})} />
setParams({...params, kelas: e.target.value})} options={KELAS_BY_JENJANG[params.jenjang]} /> setParams({...params, fase: e.target.value})} options={['A','B','C','D','E','F']} />
setParams({...params, semester: e.target.value})} options={['1 (Ganjil)', '2 (Genap)']} /> setParams({...params, alokasi: e.target.value})} />
setParams({...params, model: e.target.value})} options={MODEL_PEMBELAJARAN_OPTIONS} /> {user.isTrial && (

Sisa Percobaan: {3 - (user.usageCounts?.['modul'] || 0)} / 3

)} {errorMsg &&
{errorMsg}
}
{generated && ( <>

A. Informasi Umum

Identifikasi Murid: {content.informasi_umum?.identifikasi_murid}

Profil Lulusan: {content.informasi_umum?.profil_lulusan}

B. Komponen Inti

1. Tujuan Pembelajaran

{content.komponen_inti?.tujuan}

2. Praktik Pedagogik (Deep Learning)

{content.komponen_inti?.praktik_pedagogik}

3. Mitra Pembelajaran

{content.komponen_inti?.mitra}

4. Lingkungan Pembelajaran

{content.komponen_inti?.lingkungan}

5. Pemanfaatan Digital

{content.komponen_inti?.digital}

C. Langkah Pembelajaran

{content.langkah_pembelajaran}

D. Asesmen dan Tindak Lanjut

{content.asesmen}

Kriteria Keberhasilan Asesmen

{content.tabel_kriteria?.map((r:any, i:number) => ( ))}
Jenis AsesmenKriteriaMemenuhiBelum Memenuhi
{r.jenis}{r.kriteria}{r.memenuhi}{r.belum}

Rubrik Penilaian

{content.tabel_rubrik?.map((r:any, i:number) => ( ))}
AspekSkor 4 (Sangat Baik)Skor 3 (Baik)Skor 2 (Cukup)Skor 1 (Perlu Bimbingan)
{r.aspek}{r.skor4}{r.skor3}{r.skor2}{r.skor1}

Rencana Tindak Lanjut

{content.tabel_tindak_lanjut?.map((r:any, i:number) => ( ))}
Kategori MuridBentuk Tindak Lanjut
{r.kategori}{r.bentuk}
“Perencanaan pembelajaran ini disusun secara berkesinambungan, dengan mempertimbangkan keseimbangan beban belajar serta mendukung pembelajaran mendalam (deep learning) sepanjang tahun.”
)}
); } // ... GeneratorBankSoal ... function GeneratorBankSoal({ school, user, onPrint, onExportWord, onUpdateUser }: { school: SchoolData, user: UserData, onPrint: () => void, onExportWord: () => void, onUpdateUser: (u: UserData) => void }) { const [sections, setSections] = useState<{id: number, type: string, count: number}[]>([ { id: 1, type: "Pilihan Ganda", count: 10 } ]); const [params, setParams] = useState({ jenjang: 'SD', kelas: '4', kurikulum: 'Kurikulum Merdeka', mapel: 'IPAS', topik: 'Pencemaran Lingkungan', tujuan: 'Peserta didik dapat menganalisis dampak aktivitas manusia terhadap lingkungan.', kesulitan: 'HOTS', bloom: 'C4 - C6', konteks: 'Kehidupan Sehari-hari', bahasa: 'Indonesia' }); const [generated, setGenerated] = useState(false); const [loading, setLoading] = useState(false); const [content, setContent] = useState({}); const [errorMsg, setErrorMsg] = useState(''); const [showUpgrade, setShowUpgrade] = useState(false); const handleJenjangChange = (newJenjang: string) => { setParams({ ...params, jenjang: newJenjang, mapel: MAPEL_BY_JENJANG[newJenjang][0], kelas: KELAS_BY_JENJANG[newJenjang][0] }); }; const addSection = () => { setSections([...sections, { id: Date.now(), type: "Pilihan Ganda", count: 5 }]); }; const removeSection = (id: number) => { if(sections.length > 1) setSections(sections.filter(s => s.id !== id)); }; const updateSection = (id: number, key: string, val: any) => { setSections(sections.map(s => s.id === id ? { ...s, [key]: val } : s)); }; const handleGen = async () => { // TRIAL CHECK if (user.isTrial) { const currentUsage = user.usageCounts?.['soal'] || 0; if (currentUsage >= 3) { setShowUpgrade(true); return; } } setLoading(true); setErrorMsg(''); setGenerated(false); const partsInstruction = sections.map((s, idx) => `Bagian ${idx+1}: ${s.count} soal tipe "${s.type}".` ).join('\n'); const prompt = ` Bertindaklah sebagai AI Pembuat Soal Profesional untuk Guru ${params.jenjang} Indonesia. TUGAS: Buat Paket Soal untuk mata pelajaran ${params.mapel} Kelas ${params.kelas} ${params.jenjang} dengan struktur berikut: ${partsInstruction} Topik Utama: "${params.topik}". Konteks: ${params.konteks}. Level Kognitif: ${params.bloom}. Tingkat Kesulitan: ${params.kesulitan}. Tujuan Pembelajaran: ${params.tujuan}. PENTING: - Untuk setiap Bagian, berikan "instruksi" pengerjaan yang spesifik (misal: "Berilah tanda silang..." untuk PG). - Output harus mencakup semua bagian yang diminta. - Jika tipe "Menjodohkan" atau "Matriks", sertakan tabel HTML dalam field 'soal'. - Sertakan [Gambar: Deskripsi...] jika perlu sebagai placeholder. OUTPUT WAJIB JSON VALID (tanpa markdown): { "parts": [ { "title": "Bagian I: Pilihan Ganda", "instructions": "Berilah tanda silang (x) pada huruf A, B, C, D, atau E di depan jawaban yang paling benar!", "questions": [ { "no": 1, "soal": "...", "opsi": ["A...", "B..."], "kunci": "A", "bloom": "C4", "indikator": "...", "pembahasan": "..." } ] }, { "title": "Bagian II: Isian Singkat", "instructions": "Isilah titik-titik di bawah ini dengan jawaban yang tepat!", "questions": [...] } ] } `; try { const result = await callGeminiAI(prompt); if (result && result.parts) { setContent(result); setGenerated(true); // INCREMENT USAGE if (user.isTrial) { const newCount = (user.usageCounts?.['soal'] || 0) + 1; onUpdateUser({ ...user, usageCounts: { ...user.usageCounts, ['soal']: newCount } }); } } else { throw new Error("Format JSON dari AI tidak sesuai."); } } catch (err) { setErrorMsg("Gagal membuat soal. Pastikan koneksi internet stabil."); } finally { setLoading(false); } }; return (
{showUpgrade && setShowUpgrade(false)} />}

Konfigurasi Soal

handleJenjangChange(e.target.value)} options={JENJANG_OPTIONS} />
setParams({...params, kelas: e.target.value})} options={KELAS_BY_JENJANG[params.jenjang]} /> setParams({...params, mapel: e.target.value})} options={MAPEL_BY_JENJANG[params.jenjang]} />
setParams({...params, topik: e.target.value})} />
{sections.map((sec, idx) => (
{idx+1}. updateSection(sec.id, 'count', parseInt(e.target.value))} min="1" max="50" />
))}
setParams({...params, kesulitan: e.target.value})} options={['Mudah', 'Sedang', 'Sulit', 'HOTS', 'Campuran']} /> setParams({...params, bloom: e.target.value})} options={['C1-C2', 'C3-C4', 'C4-C6', 'Campuran']} />
setParams({...params, konteks: e.target.value})} options={KONTEKS_OPTIONS} /> {user.isTrial && (

Sisa Percobaan: {3 - (user.usageCounts?.['soal'] || 0)} / 3

)} {errorMsg &&
{errorMsg}
}
{generated && ( <> {/* Header Sekolah Resmi dengan Dinas */} {/* Identitas Siswa & Kotak Nilai */}
Nama Siswa:
Kelas / Jenjang:{params.kelas} / {params.jenjang}
Mata Pelajaran:{params.mapel}
Hari / Tanggal:
Nilai /100
{/* Looping Parts */} {content.parts?.map((part: any, pIdx: number) => (

{part.title}

{part.instructions}

{part.questions?.map((item: any, qIdx: number) => (
{item.no}.
[Ilustrasi: $1]
') .replace(/ {/* Render Opsi if available */} {item.opsi && item.opsi.length > 0 && (
{item.opsi.map((opt: any, i: number) => (
{String(opt)}
))}
)} {/* Space for Essay */} {(!item.opsi || item.opsi.length === 0) && (
)} ))} ))} {/* Kunci Jawaban (Halaman Baru) */}

KUNCI JAWABAN DAN PEDOMAN PENSKORAN

{content.parts?.map((part: any, pIdx: number) => (

{part.title}

{part.questions?.map((item: any, qIdx: number) => ( ))}
No Kunci Bloom Indikator Pembahasan
{item.no} {item.kunci} {item.bloom} {item.indikator} {item.pembahasan}
))}
)}
); } // ... DashboardLayout, LoginPage ... // (Reusing these components as is, but focusing on the main export for security logic) function DashboardLayout({ user, allUsers, schoolData, onLogout, onAddUser, onDeleteUser, onUpdateUser, onUpdateSchoolData, darkMode, setDarkMode, db, appId }: any) { const [sidebarOpen, setSidebarOpen] = useState(true); const [activeMenu, setActiveMenu] = useState('dashboard'); const [isFullscreen, setIsFullscreen] = useState(false); const toggleFullscreen = () => { if (!document.fullscreenElement) { document.documentElement.requestFullscreen().catch((err) => { console.error(`Error attempting to enable full-screen mode: ${err.message} (${err.name})`); }); setIsFullscreen(true); } else { if (document.exitFullscreen) { document.exitFullscreen(); setIsFullscreen(false); } } }; const menus = [ { id: 'dashboard', label: 'Dashboard', icon: , roles: ['admin', 'guru'] }, { id: 'jadwal', label: 'Jadwal Pelajaran', icon: , roles: ['admin', 'guru'] }, { id: 'kalender', label: 'Kalender Akademik', icon: , roles: ['admin', 'guru'] }, { id: 'user_management', label: 'Manajemen User', icon: , roles: ['admin'] }, { id: 'monitoring', label: 'Monitoring Kinerja', icon: , roles: ['admin'] }, { id: 'referensi', label: 'Panduan Kurikulum', icon: , roles: ['admin', 'guru'] }, { id: 'siswa', label: 'Data Siswa', icon: , roles: ['guru'] }, { id: 'penilaian', label: 'Penilaian Siswa', icon: , roles: ['guru'] }, { id: 'profile', label: 'Profil Saya', icon: , roles: ['guru', 'admin'] }, { id: 'pengaturan', label: 'Pengaturan Sekolah', icon: , roles: ['admin', 'guru'] }, { id: 'prota', label: 'Program Tahunan', icon: , roles: ['guru', 'admin'] }, { id: 'promes', label: 'Program Semester', icon: , roles: ['guru', 'admin'] }, { id: 'atp', label: 'Alur Tujuan Pembelajaran', icon: , roles: ['guru', 'admin'] }, { id: 'modul', label: 'Modul Ajar / RPP', icon: , roles: ['guru', 'admin'] }, { id: 'kokurikuler', label: 'Kokurikuler', icon: , roles: ['guru', 'admin'] }, { id: 'banksoal', label: 'Bank Soal', icon: , roles: ['guru', 'admin'] }, { id: 'lkpd', label: 'LKPD', icon: , roles: ['guru', 'admin'] }, ].filter(m => m.roles.includes(user.role)); const handleExportPDF = () => { const content = document.getElementById('document-preview'); if (!content) { alert("Dokumen belum dibuat."); return; } const iframe = document.createElement('iframe'); iframe.style.position = 'fixed'; iframe.style.right = '0'; iframe.style.bottom = '0'; iframe.style.width = '0'; iframe.style.height = '0'; iframe.style.border = '0'; document.body.appendChild(iframe); const doc = iframe.contentWindow?.document; if(!doc) return; doc.open(); doc.write(`${schoolData.namaSekolah}
${content.innerHTML}
`); doc.close(); }; const handleExportWord = () => { const content = document.getElementById('document-preview'); if (!content) { alert("Dokumen belum dibuat."); return; } const htmlContent = `${schoolData.namaSekolah}${content.innerHTML}`; const url = URL.createObjectURL(new Blob(['\ufeff', htmlContent], { type: 'application/msword' })); const link = document.createElement('a'); link.href = url; link.download = `Dokumen_${schoolData.namaSekolah}.doc`; document.body.appendChild(link); link.click(); document.body.removeChild(link); }; return (

{menus.find(m => m.id === activeMenu)?.label}

{activeMenu === 'dashboard' && } {activeMenu === 'user_management' && user.role === 'admin' && } {activeMenu === 'monitoring' && user.role === 'admin' && } {activeMenu === 'referensi' && } {/* Passed onPrint, onExportWord, school, user to Jadwal & Kalender */} {activeMenu === 'jadwal' && } {activeMenu === 'kalender' && } {activeMenu === 'siswa' && user.role === 'guru' && } {activeMenu === 'penilaian' && user.role === 'guru' && } {activeMenu === 'profile' && } {activeMenu === 'prota' && } {activeMenu === 'promes' && } {activeMenu === 'atp' && } {activeMenu === 'modul' && } {activeMenu === 'kokurikuler' && } {activeMenu === 'banksoal' && } {activeMenu === 'lkpd' && } {activeMenu === 'pengaturan' && }
); } function LoginPage({ onLogin, users }: { onLogin: (u: UserData) => void, users: UserData[] }) { const [inputUser, setInputUser] = useState(''); const [inputPass, setInputPass] = useState(''); const [error, setError] = useState(''); const [isLoading, setIsLoading] = useState(false); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); setIsLoading(true); setError(''); setTimeout(() => { const foundUser = users.find(u => u.username === inputUser && u.password === inputPass); if (foundUser) { onLogin(foundUser); } else { setError('Kredensial tidak valid. Silakan coba lagi.'); setIsLoading(false); } }, 800); }; return (

EduAdmin

Transformasi Digital
Administrasi Sekolah

Platform Administrasi Sekolah Multi-Jenjang (PAUD, SD, SMP, SMA, SMK) berbasis AI.

+2k

Digunakan oleh ribuan guru profesional

Kemendikdasmen

Selamat Datang

Masuk ke akun Anda untuk melanjutkan.

setInputUser(e.target.value)} required />
setInputPass(e.target.value)} required />
{error &&
{error}
}

© 2026 EduAdmin Universal. v4.5 All Levels

); } export default function App() { const firebaseConfig = JSON.parse(typeof __firebase_config !== 'undefined' ? __firebase_config : '{}'); const app = initializeApp(firebaseConfig); const auth = getAuth(app); const db = getFirestore(app); const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id'; const [activeUser, setActiveUser] = useState(null); const [users, setUsers] = useState(INITIAL_USERS); const [schoolData, setSchoolData] = useState(INITIAL_SCHOOL_DATA); const [darkMode, setDarkMode] = useState(false); const [firebaseUser, setFirebaseUser] = useState(null); const [isDbReady, setIsDbReady] = useState(false); const [warning, setWarning] = useState(false); // --- SECURITY LOCK IMPLEMENTATION --- useEffect(() => { // 1. Disable Right Click const handleContext = (e: MouseEvent) => { e.preventDefault(); setWarning(true); setTimeout(() => setWarning(false), 2000); }; // 2. Disable Specific Keys const handleKeyDown = (e: KeyboardEvent) => { // Block F12, Ctrl+Shift+I, Ctrl+Shift+J, Ctrl+U, Ctrl+C, Ctrl+S, Ctrl+P if ( e.key === 'F12' || (e.ctrlKey && e.shiftKey && (e.key === 'I' || e.key === 'J')) || (e.ctrlKey && (e.key === 'u' || e.key === 'U')) || (e.ctrlKey && (e.key === 's' || e.key === 'S')) || (e.ctrlKey && (e.key === 'c' || e.key === 'C')) ) { e.preventDefault(); setWarning(true); setTimeout(() => setWarning(false), 2000); } }; document.addEventListener('contextmenu', handleContext); document.addEventListener('keydown', handleKeyDown); return () => { document.removeEventListener('contextmenu', handleContext); document.removeEventListener('keydown', handleKeyDown); }; }, []); useEffect(() => { const initAuth = async () => { if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) { await signInWithCustomToken(auth, __initial_auth_token); } else { await signInAnonymously(auth); } }; initAuth(); const unsubscribe = onAuthStateChanged(auth, (u) => setFirebaseUser(u)); return () => unsubscribe(); }, [auth]); useEffect(() => { if (!firebaseUser) return; const usersCollection = collection(db, 'artifacts', appId, 'public', 'data', 'users'); const unsubUsers = onSnapshot(usersCollection, (snapshot) => { const loadedUsers = snapshot.docs.map(d => d.data() as UserData); if (loadedUsers.length === 0) { INITIAL_USERS.forEach(u => setDoc(doc(db, 'artifacts', appId, 'public', 'data', 'users', u.id), u)); } else { setUsers(loadedUsers); } setIsDbReady(true); }, (error) => console.error("Sync Users Error:", error)); const settingsDoc = doc(db, 'artifacts', appId, 'public', 'data', 'settings', 'school_data'); const unsubSettings = onSnapshot(settingsDoc, (docSnap) => { if (docSnap.exists()) { setSchoolData(docSnap.data() as SchoolData); } else { setDoc(settingsDoc, INITIAL_SCHOOL_DATA); } }, (error) => console.error("Sync Settings Error:", error)); return () => { unsubUsers(); unsubSettings(); }; }, [firebaseUser, db, appId]); const handleLogin = (u: UserData) => setActiveUser({ ...u, isLoggedIn: true }); const handleLogout = () => setActiveUser(null); const handleAddUser = async (newUser: UserData) => { if (!firebaseUser) return; await setDoc(doc(db, 'artifacts', appId, 'public', 'data', 'users', newUser.id), newUser); }; const handleDeleteUser = async (id: string) => { if (!firebaseUser) return; await deleteDoc(doc(db, 'artifacts', appId, 'public', 'data', 'users', id)); }; const handleUpdateUser = async (updatedUser: UserData) => { if (!firebaseUser) return; await setDoc(doc(db, 'artifacts', appId, 'public', 'data', 'users', updatedUser.id), updatedUser, { merge: true }); // Update local activeUser if it's the current user, to reflect changes immediately in UI (like trial count) if (activeUser?.id === updatedUser.id) { setActiveUser(prev => prev ? ({ ...prev, ...updatedUser }) : null); } }; const handleUpdateSchoolData = async (newData: SchoolData) => { if (!firebaseUser) return; await setDoc(doc(db, 'artifacts', appId, 'public', 'data', 'settings', 'school_data'), newData); }; useEffect(() => { document.documentElement.classList.toggle('dark', darkMode); }, [darkMode]); if (!isDbReady) { return

Menghubungkan Database...

; } return ( // Security Wrapper: select-none disables text selection, onDragStart disable
e.preventDefault()} onCut={(e) => e.preventDefault()} onDragStart={(e) => e.preventDefault()} > {/* Style to ensure printing still works even if user-select is none on screen */} {/* Warning Toast */} {warning && (
Fitur Copy/Paste & Inspeksi Dinonaktifkan untuk Keamanan!
)} {!activeUser?.isLoggedIn ? ( ) : ( )}
); }